<?php

namespace WeForge\Runtime\Fpm;

use Exception;
use Swoole\Coroutine\FastCGI\Client;
use Swoole\Process;
use Throwable;
use WeForge\Runtime\Response;

class Fpm
{
    const PID_FILE = '/tmp/.weforge/php-fpm.pid';
    const SOCKET = '/tmp/.weforge/php-fpm.sock';
    const CONFIG = '/tmp/.weforge/php-fpm.conf';

    /**
     * The PHP-FPM process.
     *
     * @var \Swoole\Process
     */
    protected $fpm;

    /**
     * Handle the incoming request.
     *
     * @param \Swoole\Http\Request $request
     *
     * @return \WeForge\Runtime\Fpm\FpmResponse
     */
    public function handle($request)
    {
        return new FpmResponse(
            (new Client('unix:'.static::SOCKET))->execute($request)
        );
    }

    /**
     * @param  \Throwable $error
     *
     * @return \WeForge\Runtime\Response
     */
    public static function handleException(Throwable $error)
    {
        return new Response([
            'message' => $error->getMessage(),
        ], 500);
    }

    /**
     * Boot FPM
     *
     * @return static
     */
    public static function boot()
    {
        if (file_exists(static::SOCKET)) {
            @unlink(static::SOCKET);
        }

        return tap(new static, function ($fpm) {
            $fpm->start();
        });
    }

    /**
     * Start the PHP-FPM process.
     *
     * @return void
     */
    public function start()
    {
        if ($this->isReady()) {
            $this->killExistingFpm();
        }

        fwrite(STDERR, 'Ensuring ready to start FPM'.PHP_EOL);

        $this->ensureReadyToStart();

        $this->fpm = new Process(function ($worker) {
            $worker->exec('/home/layer/bin/php-fpm', [
                '--nodaemonize',
                '--force-stderr',
                '--fpm-config',
                static::CONFIG,
            ]);
        });

        fwrite(STDERR, 'Starting FPM Process...'.PHP_EOL);

        $this->fpm->start();

        $this->ensureFpmHasStarted();
    }

    /**
     * Ensure that the proper configuration is in place to start FPM.
     *
     * @return void
     */
    protected function ensureReadyToStart()
    {
        if (! is_dir(dirname(self::SOCKET))) {
            mkdir(dirname(self::SOCKET));
        }

        if (! file_exists(self::CONFIG)) {
            file_put_contents(
                self::CONFIG,
                file_get_contents(__DIR__.'/../../../stubs/php-fpm.conf')
            );
        }
    }

    /**
     * Kill any existing FPM processes on the system.
     *
     * @return void
     */
    protected function killExistingFpm()
    {
        fwrite(STDERR, 'Killing existing FPM'.PHP_EOL);

        if (! file_exists(static::PID_FILE)) {
            return unlink(static::SOCKET);
        }

        $pid = (int) file_get_contents(static::PID_FILE);

        if (! $this->processExists($pid)) {
            return $this->removeFpmProcessFiles();
        }

        $result = Process::kill($pid, SIGTERM);

        if ($result === false) {
            return $this->removeFpmProcessFiles();
        }

        $this->waitUntilStopped($pid);

        $this->removeFpmProcessFiles();
    }

    /**
     * Remove FPM's process related files.
     *
     * @return void
     */
    protected function removeFpmProcessFiles()
    {
        unlink(static::SOCKET);
        unlink(static::PID_FILE);
    }

    /**
     * Wait until the given process is stopped.
     *
     * @param  int  $pid
     * @return void
     */
    protected function waitUntilStopped($pid)
    {
        $elapsed = 0;

        while ($this->processExists($pid)) {
            usleep(5000);

            $elapsed += 5000;

            if ($elapsed > 1000000) {
                throw new Exception('Process did not stop within the given threshold.');
            }
        }
    }

    /**
     * Determine is the FPM process is ready to receive requests.
     *
     * @return bool
     */
    protected function isReady()
    {
        clearstatcache(false, self::SOCKET);

        return file_exists(self::SOCKET);
    }

    /**
     * Ensure that the FPM process is still running.
     *
     * @return void
     *
     * @throws \Exception
     */
    public function ensureRunning()
    {
        if (! $this->fpm || ! $this->processExists($this->fpm->pid)) {
            throw new Exception('PHP-FPM has stopped unexpectedly.');
        }
    }

    /**
     * Check if the process exists
     *
     * @param  int $pid
     *
     * @return bool
     */
    protected function processExists($pid)
    {
        return Process::kill($pid, 0);
    }

    /**
     * Wait until the FPM process is ready to receive requests.
     *
     * @return void
     */
    protected function ensureFpmHasStarted()
    {
        $elapsed = 0;

        while (! $this->isReady()) {
            usleep(5000);

            $elapsed += 5000;

            if ($elapsed > ($fiveSeconds = 5000000)) {
                throw new Exception('Timed out waiting for FPM to start: '.static::SOCKET);
            }

            if (! $this->processExists($this->fpm->pid)) {
                throw new Exception('PHP-FPM was unable to start.');
            }
        }
    }
}
